﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Mvc;
using System.Diagnostics.CodeAnalysis;
using System.Collections;

namespace Presentation.Web.Core.ModelBinder
{
    /// <summary>
    /// This model binder binds list object without re-creating items.
    /// </summary>
    public class CommonDefaultModelBinder : DefaultModelBinder
    {
        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException("bindingContext");
            }

            object model = bindingContext.Model;
            Type modelType = bindingContext.ModelType;
            Type listType = ExtractGenericInterface(modelType, typeof(IList<>));

            // Fallback to default behavior if the model is null or the model is not a type of list
            if (model == null || listType == null)
            {
                return base.BindModel(controllerContext, bindingContext);
            }

            Type elementType = listType.GetGenericArguments()[0];

            ModelBindingContext collectionBindingContext = new ModelBindingContext()
            {
                ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, modelType),
                ModelName = bindingContext.ModelName,
                ModelState = bindingContext.ModelState,
                PropertyFilter = bindingContext.PropertyFilter,
                ValueProvider = bindingContext.ValueProvider
            };

            return BindList(controllerContext, collectionBindingContext, elementType);
        }

        internal object BindList(ControllerContext controllerContext, ModelBindingContext bindingContext, Type elementType)
        {
            IModelBinder elementBinder = Binders.GetBinder(elementType);
            object model = bindingContext.Model;

            // get an IList instance of the model
            IList modelList = ListHelpers.GetList(model, elementType);
            int modelCount = modelList.Count;
            int currentIndex = 0;
            for (; ; currentIndex++)
            {
                string subIndexKey = CreateSubIndexName(bindingContext.ModelName, currentIndex);
                if (!bindingContext.ValueProvider.ContainsPrefix(subIndexKey))
                {
                    // we ran out of elements to pull
                    break;
                }

                object element = currentIndex < modelCount ? modelList[currentIndex] : null;
                ModelBindingContext innerContext = new ModelBindingContext()
                {
                    ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => element, elementType),
                    ModelName = subIndexKey,
                    ModelState = bindingContext.ModelState,
                    PropertyFilter = bindingContext.PropertyFilter,
                    ValueProvider = bindingContext.ValueProvider
                };
                object thisElement = elementBinder.BindModel(controllerContext, innerContext);

                // we need to merge model errors up
                AddValueRequiredMessageToModelState(controllerContext, bindingContext.ModelState, subIndexKey, elementType, thisElement);

                if (currentIndex >= modelCount)
                {
                    modelList.Add(thisElement);
                }
                else if (!Object.Equals(element, thisElement))
                {
                    modelList[currentIndex] = thisElement;
                }
            }

            // remove extra elements
            while (currentIndex < modelList.Count)
            {
                modelList.RemoveAt(modelList.Count - 1);
            }

            return model;
        }

        private static void AddValueRequiredMessageToModelState(ControllerContext controllerContext, ModelStateDictionary modelState, string modelStateKey, Type elementType, object value)
        {
            if (value == null && !TypeAllowsNullValue(elementType) && modelState.IsValidField(modelStateKey))
            {
                modelState.AddModelError(modelStateKey, "A value is required.");
            }
        }

        private static bool TypeAllowsNullValue(Type type)
        {
            return (!type.IsValueType || IsNullableValueType(type));
        }

        private static bool IsNullableValueType(Type type)
        {
            return Nullable.GetUnderlyingType(type) != null;
        }

        private static Type ExtractGenericInterface(Type queryType, Type interfaceType)
        {
            Func<Type, bool> matchesInterface = t => t.IsGenericType && t.GetGenericTypeDefinition() == interfaceType;
            return (matchesInterface(queryType)) ? queryType : queryType.GetInterfaces().FirstOrDefault(matchesInterface);
        }

        private static class ListHelpers
        {
            public static IList GetList(object listObject, Type elementType)
            {
                IList list = listObject as IList;
                if (list != null)
                {
                    return list;
                }

                Type wrapperType = typeof(ListWrapper<>).MakeGenericType(elementType);
                list = (IList)Activator.CreateInstance(wrapperType, listObject);
                return list;
            }

            private class ListWrapper<T> : IList
            {
                private IList<T> _list;

                public ListWrapper(IList<T> list)
                {
                    _list = list;
                }

                int System.Collections.IList.Add(object value) { _list.Add((T)value); return _list.Count - 1; }

                object System.Collections.IList.this[int index]
                {
                    get { return (T)_list[index]; }
                    set { _list[index] = (T)value; }
                }

                int System.Collections.ICollection.Count { get { return _list.Count; } }

                void System.Collections.IList.RemoveAt(int index) { _list.RemoveAt(index); }

                #region Not Implemented

                void System.Collections.IList.Clear() { throw new NotImplementedException(); }

                bool System.Collections.IList.Contains(object value) { throw new NotImplementedException(); }

                int System.Collections.IList.IndexOf(object value) { throw new NotImplementedException(); }

                void System.Collections.IList.Insert(int index, object value) { throw new NotImplementedException(); }

                bool System.Collections.IList.IsFixedSize { get { throw new NotImplementedException(); } }

                bool System.Collections.IList.IsReadOnly { get { throw new NotImplementedException(); } }

                void System.Collections.IList.Remove(object value) { throw new NotImplementedException(); }

                void System.Collections.ICollection.CopyTo(Array array, int index) { throw new NotImplementedException(); }

                bool System.Collections.ICollection.IsSynchronized { get { throw new NotImplementedException(); } }

                object System.Collections.ICollection.SyncRoot { get { throw new NotImplementedException(); } }

                System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw new NotImplementedException(); }

                #endregion Not Implemented
            }

        }
    }
}
